package cas.ihep.hadoop.util;

import org.luaj.vm2.LuaBoolean;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.lib.OneArgFunction;
import org.luaj.vm2.lib.VarArgFunction;

import javax.script.*;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

public class HadoopLuaEval {
    private static class MyVarargFunc extends VarArgFunction{
        @Override
        public Varargs invoke(Varargs args) {
            switch (args.narg()){
                case 0:
                    return call();
                case 1:
                    return call(args.arg1());
                case 2:
                    return call(args.arg1(),args.arg(2));
                case 3:
                    return call(args.arg1(),args.arg(2),args.arg(3));
                case 4:
                    return call(args.arg1(),args.arg(2),args.arg(3),args.arg(4));
                default:
                    return LuaValue.NIL;
            }
        }
    }

    public static Bindings evalOop(ScriptEngine e,Reader reader,HadoopLuaOopEngine engine)throws ScriptException{
        Bindings bd;
        evalOopInternal(e,reader,engine,bd=new SimpleBindings());
        return bd;
    }

    public static void evalOop(ScriptEngine e,Reader reader,HadoopLuaOopEngine engine,Bindings bd)throws ScriptException{
        evalOopInternal(e,reader,engine,bd);
    }

    private static void evalOopInternal(ScriptEngine e, Reader reader,final HadoopLuaOopEngine engine, final Bindings bd) throws ScriptException {
        final LuaTable hepFileClass=new LuaTable();
        final LuaTable metaClass=new LuaTable();
        metaClass.set(LuaValue.valueOf("__index"),hepFileClass);
        hepFileClass.set(LuaValue.valueOf("baseName"), new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isuserdata()){
                    Object obj=arg.touserdata();
                    if(obj==null){
                        return LuaValue.error("arg is NIL");
                    }else if(obj instanceof HadoopLuaOopEngine.HepFile) {
                        return LuaValue.valueOf(((HadoopLuaOopEngine.HepFile) obj).baseName());
                    }
                }
                return LuaValue.error("Invalid type of arg: "+arg.typename());
            }
        });
        hepFileClass.set(LuaValue.valueOf("fileName"), new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isuserdata()){
                    Object obj=arg.touserdata();
                    if(obj==null){
                        return LuaValue.error("arg is NIL");
                    }
                    if(obj instanceof HadoopLuaOopEngine.HepFile) {
                        return LuaValue.valueOf(((HadoopLuaOopEngine.HepFile) obj).fileName());
                    }
                }
                return LuaValue.error("Invalid type of arg: "+arg.typename());
            }
        });
        hepFileClass.set(LuaValue.valueOf("path"), new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isuserdata()){
                    Object obj=arg.touserdata();
                    if(obj==null){
                        return LuaValue.error("arg is NIL");
                    }
                    if(obj instanceof HadoopLuaOopEngine.HepFile) {
                        return LuaValue.valueOf(((HadoopLuaOopEngine.HepFile) obj).path());
                    }
                }
                return LuaValue.error("Invalid type of arg: "+arg.typename());
            }
        });
        hepFileClass.set(LuaValue.valueOf("absolutePath"), new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isuserdata()){
                    Object obj=arg.touserdata();
                    if(obj==null){
                        return LuaValue.error("arg is NIL");
                    }
                    if(obj instanceof HadoopLuaOopEngine.HepFile) {
                        return LuaValue.valueOf(((HadoopLuaOopEngine.HepFile) obj).absolutePath());
                    }
                }
                return LuaValue.error("Invalid type of arg: "+arg.typename());
            }
        });
        hepFileClass.set(LuaValue.valueOf("fd"), new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isuserdata()){
                    Object obj=arg.touserdata();
                    if(obj==null){
                        return LuaValue.error("arg is NIL");
                    }
                    if(obj instanceof HadoopLuaOopEngine.HepFile){
                        return LuaValue.valueOf(((HadoopLuaOopEngine.HepFile)obj).fd());
                    }
                }
                return LuaValue.error("Invalid type of arg: "+arg.typename());
            }
        });
        hepFileClass.set(LuaValue.valueOf("persist"), new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isuserdata()){
                    Object obj=arg.touserdata();
                    if(obj==null){
                        return LuaValue.error("arg is NIL");
                    }
                    if(obj instanceof HadoopLuaOopEngine.HepFile){
                        ((HadoopLuaOopEngine.HepFile)obj).persist();
                    }
                }
                return LuaValue.NIL;
            }
        });
        hepFileClass.set(LuaValue.valueOf("delete"), new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isuserdata()){
                    Object obj=arg.touserdata();
                    if(obj==null){
                        return LuaValue.error("arg is NIL");
                    }
                    if(obj instanceof HadoopLuaOopEngine.HepFile){
                        ((HadoopLuaOopEngine.HepFile)obj).delete();
                    }
                }
                return LuaValue.NIL;
            }
        });
        hepFileClass.set(LuaValue.valueOf("free"), new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isuserdata()){
                    Object obj=arg.touserdata();
                    if(obj==null){
                        return LuaValue.error("arg is NIL");
                    }
                    if(obj instanceof HadoopLuaOopEngine.HepFile){
                        ((HadoopLuaOopEngine.HepFile)obj).free();
                    }
                }
                return LuaValue.NIL;
            }
        });
        hepFileClass.set(LuaValue.valueOf("complete"), new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isuserdata()){
                    Object obj=arg.touserdata();
                    if(obj==null){
                        return LuaValue.error("arg is NIL");
                    }
                    if(obj instanceof HadoopLuaOopEngine.HepFile){
                        try {
                            ((HadoopLuaOopEngine.HepFile) obj).complete();
                        }catch (Exception e){
                            return LuaValue.error(e.getMessage());
                        }
                    }
                }
                return LuaValue.NIL;
            }
        });
        bd.put("hadoop_getInput",new MyVarargFunc(){

            private LuaValue call(int index){
                try{
                    return LuaValue.userdataOf(engine.hadoop_getInput(index)).setmetatable(metaClass);
                }catch (IndexOutOfBoundsException iobe){
                    return LuaValue.error("Index out of bounds: "+index);
                }
            }

            public LuaValue call(){
                return call(1);
            }

            public LuaValue call(LuaValue index){
                if(index.isint()){
                    return call(index.toint());
                }
                return LuaValue.error("Invalid type of index: "+index.typename());
            }
        });
        bd.put("hadoop_getOutput",new MyVarargFunc(){
            private LuaValue call(int index){
                try{
                    return LuaValue.userdataOf(engine.hadoop_getOutput(index)).setmetatable(metaClass);
                }catch (IndexOutOfBoundsException iobe){
                    return LuaValue.error("Index out of bounds: "+index);
                }
            }
            public LuaValue call(){
                return call(1);
            }
            public LuaValue call(LuaValue arg){
                if(arg.isint()){
                    return call(arg.toint());
                }
                return LuaValue.error("Invalid type of index: "+arg.typename());
            }
        });
        bd.put("hadoop_run", new MyVarargFunc() {
            private LuaValue call(String cmd,HadoopRunConfig config){
                if(config==null){
                    config=new HadoopRunConfig();
                }
                try {
                    return LuaValue.valueOf(engine.hadoop_run(cmd, config));
                }catch (Exception e){
                    return LuaValue.error("Java exception: "+e.getMessage());
                }
            }
            public LuaValue call(LuaValue arg1){
                if(arg1.isstring()){
                    return call(arg1.tojstring(),null);
                }
                return LuaValue.error("Invalid type of cmd: "+arg1.typename());
            }
            @Override
            public LuaValue call(LuaValue arg1, LuaValue arg2) {
                if(arg1.isstring()){
                    HadoopRunConfig config=null;
                    if(arg2.istable()){
                        config=new HadoopRunConfig((LuaTable) arg2);
                    }
                    return call(arg1.tojstring(),config);
                }
                return LuaValue.error("Invalid type of cmd: "+arg1.typename());
            }
        });
        bd.put("hadoop_addOutput", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isstring()){
                    engine.hadoop_addOutput(arg.tojstring());
                    return LuaValue.NIL;
                }
                return LuaValue.error("Invalid type of cmd: "+arg.typename());
            }
        });
        bd.put("hadoop_addOutputs", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.istable()){
                    LuaTable tb=(LuaTable)arg;
                    int sz=tb.keyCount();
                    for(int i=0;i<sz;i++){
                        LuaValue val=tb.get(i+1);
                        if(val.isstring()){
                            engine.hadoop_addOutput(val.tojstring());
                        }
                    }
                    return LuaValue.NIL;
                }
                return LuaValue.error("Invalid type of cmd: "+arg.typename());
            }
        });
        bd.put("HepFile",hepFileClass);
        e.eval(reader,bd);
    }

    private static void evalInternal(CompiledScript cs,final HadoopLuaEngine engine,Bindings bd)throws ScriptException{
        //final LuaTable hadoop_outputname=new LuaTable();
        //bd.put("Hadoop_InputName",engine.hadoop_value("Hadoop_InputName"));
        //bd.put("Hadoop_OutputName",hadoop_outputname);
        bd.put("hadoop_addOutput", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isstring()) {
                    try {
                        engine.hadoop_addOutput(arg.tojstring());
                        //hadoop_outputname.set(hadoop_outputname.keyCount()+1,arg);
                        return LuaValue.NIL;
                    }catch (Exception e){
                        return LuaValue.error("Java exception: "+e.getMessage());
                    }
                }
                return LuaValue.error("Invalid type of arg");
            }
        });
        bd.put("hadoop_addOutputs", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.istable()) {
                    LuaTable table=(LuaTable)arg;
                    int sz=table.keyCount();
                    try {
                        for(int i=0;i<sz;i++) {
                            LuaValue val=table.get(i+1);
                            if(val.isstring()) {
                                engine.hadoop_addOutput(val.tojstring());
                                //hadoop_outputname.set(hadoop_outputname.keyCount()+1,val);
                            }else{
                                return LuaValue.error("Invalid type of arg");
                            }
                        }
                        return LuaValue.NIL;
                    }catch (Exception e){
                        return LuaValue.error("Java exception: "+e.getMessage());
                    }
                }
                return LuaValue.error("Invalid type of arg");
            }
        });
        bd.put("hadoop_getInputFile", new MyVarargFunc() {
            public LuaValue call(){
                return LuaValue.valueOf(engine.hadoop_getInputFile(1));
            }

            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isint()){
                    return LuaValue.valueOf(engine.hadoop_getInputFile(arg.toint()));
                }
                return LuaValue.error("Invalid type of arg");
            }
        });
        bd.put("hadoop_run", new MyVarargFunc() {
            public LuaValue call(LuaValue arg){
                if(arg.isstring()){
                    try {
                        return LuaValue.valueOf(engine.hadoop_run(arg.tojstring(), new HadoopRunConfig()));
                    }catch (Exception e){
                        return LuaValue.error("Java exception: "+e.getMessage());
                    }
                }
                return LuaValue.error("Invalid type of args");
            }
            public LuaValue call(LuaValue arg0,LuaValue arg1){
                if(arg0.isstring()){
                    try {
                        return LuaValue.valueOf(engine.hadoop_run(arg0.tojstring(), new HadoopRunConfig(arg1.istable()?(LuaTable)arg1:null)));
                    }catch (Exception e){
                        return LuaValue.error("Java exception: "+e.getMessage());
                    }
                }
                return LuaValue.error("Invalid type of args");
            }
        });
        bd.put("hadoop_value", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isstring()){
                    return engine.hadoop_value(arg.tojstring());
                }
                return LuaValue.error("Invalid type of args");
            }
        });
        bd.put("hadoop_getOutputFile", new MyVarargFunc() {
            public LuaValue call(){
                return LuaValue.valueOf(engine.hadoop_getOutputFile(1));
            }
            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isint()){
                    String str=engine.hadoop_getOutputFile(arg.toint());
                    return str!=null?LuaValue.valueOf(str):LuaValue.NIL;
                }
                return LuaValue.error("Invalid type of args");
            }
        });
        bd.put("hadoop_getOutput", new MyVarargFunc() {
            @Override
            public LuaValue call() {
                return LuaValue.valueOf(engine.hadoop_getOutput(1));
            }

            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isint()){
                    return LuaValue.valueOf(engine.hadoop_getOutput(arg.toint()));
                }
                return LuaValue.error("Invalid type of args");
            }
        });
        bd.put("hadoop_getInput", new MyVarargFunc() {
            @Override
            public LuaValue call() {
                return LuaValue.valueOf(engine.hadoop_getInput(1));
            }

            @Override
            public LuaValue call(LuaValue arg) {
                if(arg.isint()) {
                    return LuaValue.valueOf(engine.hadoop_getInput(arg.toint()));
                }
                return LuaValue.error("Invalid type of args");
            }
        });
        cs.eval(bd);
    }

    public static void eval(CompiledScript cs, final HadoopLuaEngine engine) throws ScriptException {
        evalInternal(cs,engine,new SimpleBindings());
    }

    public static void eval(CompiledScript cs,final HadoopLuaEngine engine,Bindings bd)throws ScriptException{
        if(bd==null){
            bd=new SimpleBindings();
        }
        evalInternal(cs,engine,bd);
    }

    private static Object convertLuaValue(LuaValue val){
        if(val.isint()){
            return val.toint();
        }else if(val.isstring()){
            return val.tojstring();
        }else if(val.islong()){
            return val.tolong();
        }else if(val.isnumber()){
            return val.todouble();
        }else if(val.isboolean()){
            return val.toboolean();
        }else if(val.istable()){
            return convertTableForSerialize((LuaTable) val);
        }
        throw new RuntimeException("Invalid convertion type");
    }

    private static Map<Object,Object> convertTableForSerialize(LuaTable tb){
        Map<Object,Object> ret=new HashMap<>();
        LuaValue keys[]=tb.keys();
        for(LuaValue k:keys){
            LuaValue v=tb.get(k);
            ret.put(convertLuaValue(k),convertLuaValue(v));
        }
        return ret;
    }

    public static Map<String,Object> convertForSerialize(Map<String,Object> bindings){
        for(Map.Entry<String,Object> entry:bindings.entrySet()){
            Object obj=entry.getValue();
            if(obj instanceof LuaValue){
                entry.setValue(convertLuaValue((LuaValue)obj));
            }
        }
        return bindings;
    }

    public static LuaValue convertObjectValue(Object obj){
        if(obj instanceof Integer){
            return LuaValue.valueOf((Integer)obj);
        }else if(obj instanceof Long){
            return LuaValue.valueOf((Long)obj);
        }else if(obj instanceof String){
            return LuaValue.valueOf((String)obj);
        }else if(obj instanceof Double){
            return LuaValue.valueOf((Double)obj);
        }else if(obj instanceof Boolean){
            return LuaValue.valueOf((Boolean)obj);
        }else if(obj instanceof Map){
            //noinspection unchecked
            return convertTableForBinding((Map<Object,Object>)obj);
        }
        return LuaValue.error("Invalid convertion type");
    }

    private static LuaTable convertTableForBinding(Map<Object,Object> map){
        LuaTable tb=new LuaTable();
        for(Map.Entry<Object,Object> entry:map.entrySet()){
            tb.set(convertObjectValue(entry.getKey()),convertObjectValue(entry.getValue()));
        }
        return tb;
    }

    public static Map<String,Object> convertForBindings(Map<String,Object> globalValues){
        for(Map.Entry<String,Object> entry:globalValues.entrySet()){
           Object obj=entry.getValue();
           if(obj instanceof Boolean){
               entry.setValue(LuaBoolean.valueOf((Boolean)obj));
           }else if(obj instanceof Map){
               //noinspection unchecked
               entry.setValue(convertTableForBinding((Map<Object,Object>)obj));
           }
        }
        return globalValues;
    }

    private static Integer parseBin(CharSequence str, int start, int end){
        int sum=0;
        for(int i=start;i<end;i++){
            char ch=str.charAt(i);
            if(ch=='0' || ch=='1') {
                sum <<= 1;
                sum+=(ch-'0');
            }else{
                return null;
            }
        }
        return sum;
    }

    private static Integer parseHex(CharSequence str, int start, int end){
        int sum=0;
        for(int i=start;i<end;i++){
            char ch=str.charAt(i);
            if(ch>='0' && ch<='9'){
                sum<<=4;
                sum+=(ch-'0');
            }else if(ch>='A' && ch<='F'){
                sum<<=4;
                sum+=(ch-'A'+10);
            }else if(ch>='a' && ch<='f'){
                sum<<=4;
                sum+=(ch-'a'+10);
            }else{
                return null;
            }
        }
        return sum;
    }

    private static Integer parseOct(CharSequence str, int start, int end){
        int sum=0;
        for(int i=start;i<end;i++){
            char ch=str.charAt(i);
            if(ch>='0' && ch<='7'){
                sum<<=3;
                sum+=(ch-'0');
            }else{
                return null;
            }
        }
        return sum;
    }

    private static Integer parseDec(CharSequence str,int start,int end,boolean isNegative){
        int base=1;
        int sum=0;
        for(int i=end-1;i>=start;i--){
            char ch=str.charAt(i);
            if(ch<='9' && ch>='0'){
                sum+=(ch-'0')*base;
                base*=10;
            }else{
                return null;
            }
        }
        return isNegative?-sum:sum;
    }

    private static final String hexDigit="0x[0-9A-Fa-f]+";
    private static final String decDigit="^-?[1-9]\\d*$";
    private static final String octDigit="0[0-7]+";
    private static final String binDigit="0b[0-1]+";
    private static final String floatDigit="^-?[1-9]\\d*\\.\\d*|-0\\.\\d*[1-9]\\d*$";
    private static final Pattern hexPattern=Pattern.compile(hexDigit);
    private static final Pattern decPattern=Pattern.compile(decDigit);
    private static final Pattern octPattern=Pattern.compile(octDigit);
    private static final Pattern binPattern=Pattern.compile(binDigit);
    private static final Pattern floatPattern=Pattern.compile(floatDigit);

    public static Object tryParse(String str){
        if("true".equals(str)){
            return LuaBoolean.TRUE;
        }
        if("false".equals(str)){
            return LuaBoolean.FALSE;
        }
        Object obj;
        if(decPattern.matcher(str).matches()){
            return Integer.valueOf(str);
        }else if(hexPattern.matcher(str).matches() && (obj=parseHex(str,2,str.length()))!=null){
            return obj;
        }else if(octPattern.matcher(str).matches() && (obj=parseOct(str,1,str.length()))!=null){
            return obj;
        }else if(binPattern.matcher(str).matches() && (obj=parseBin(str,2,str.length()))!=null){
            return obj;
        }else if(floatPattern.matcher(str).matches()){
            return Double.valueOf(str);
        }
        return str;
    }

    public static void addExternalVariable(Bindings bd,Iterable<Map.Entry<String,String>> vars){
        for(Map.Entry<String,String> entry:vars){
            String key=entry.getKey();
            if(key.indexOf('.')==-1){
                bd.put(key,HadoopLuaEval.tryParse(entry.getValue()));
            }
        }
    }
}
